import fudge
from fudge.events import WindowResize
from fudge.core import *
from fudge.resources import Texture, FBO, Shader, ShaderProgram, NoTexture, TextureGroup, Stacks
from fudge.models import Model, Empty
from fudge.types import Matrix4, Vector4, Vector3
from default import DefaultRenderer
import Image
import lights as LIGHTS


''' 
    activating debug mode allows real-time switching of certain features,
    by removing comments from certain commented if commands. However, this can result in a
    performance drop
'''
DEBUG_MODE = False

'''
    Maximum number of lights. Cannot be changed at the moment, since it is
    hard coded in the shader.
'''
MAX_LIGHTS = 12


class DeferredRenderer(DefaultRenderer):
    '''
        Fudge Deferred Shader implementation
        
        The G-buffer is created via deferred_gbuffer shader programs.
    '''
    def __init__(self, debug_mode = False, width = 640, height = 480):
        global DEBUG_MODE
        DEBUG_MODE = debug_mode
        self.width = width
        self.height = height
        
        # default lights
        self._lights = [Vector4() for x in range(MAX_LIGHTS)]
        self._lights_color = [Vector3() for x in range(MAX_LIGHTS)]
        self._lights_dist = [0.0 for x in range(MAX_LIGHTS)]
        self._lights_spec = [1.0 for x in range(MAX_LIGHTS)]
        
    def __buildFBOs(self):
        # create FBO for G-buffer with following targets
            # 0 - Albedo + Tangent space surface height
            # 1 - Normal + Eye space depth 
            # 2 - Shading factors
            # 3 - Modelview position coordinates
        fbo = FBO(width = self.width, height = self.width, depth = True)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.addTarget(format = GL_RGBA32F, mode = GL_RGBA, type = GL_FLOAT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST)
        fbo.addTarget(format = GL_RGB32F, mode = GL_RGB, type = GL_FLOAT, mag_filter = GL_NEAREST)
        fbo.check()
        self.gbuffer_fbo = fbo
        
        
        # create FBO for 1st Pass: deferred lighting and SSAO
            # 0 - All Lighting + AO
            # 1 - Overbright channel + edge detect
        fbo = FBO(width = self.width, height = self.width, depth = False)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.check()
        self.pass1_fbo = fbo
        
        # FBO pass 2: horizontal blur, run this half-res
        fbo = FBO(width = self.width / 2, height = self.height / 2, depth = False)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_NEAREST, wrap = GL_CLAMP)
        fbo.check()
        self.pass2_fbo = fbo
        
        # FBO pass 3: vertical blur, run this half-res but use GL_LINEAR interpol for upscaling in next pass
        fbo = FBO(width = self.width / 2, height = self.height / 2, depth = False)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_LINEAR, wrap = GL_CLAMP)
        fbo.addTarget(format = GL_RGBA8, mode = GL_RGBA, type = GL_UNSIGNED_INT, mag_filter = GL_LINEAR, wrap = GL_CLAMP)
        fbo.check()
        self.pass3_fbo = fbo
        
    def __compileShaders(self):
        global DEBUG_MODE
        def decommenter(source):
            return source.replace('//++','') if DEBUG_MODE else source
            
        self.gbuffer_program = ShaderProgram.fromFiles('fudge/renderers/deferred_gbuffer', decommenter)
        flat_vs = Shader.fromFile('fudge/renderers/deferred_flat.vert')
        self.pass1_program = ShaderProgram(flat_vs, Shader.fromFile('fudge/renderers/deferred_pass1.frag', decommenter))        
        self.pass2_program = ShaderProgram(flat_vs, Shader.fromFile('fudge/renderers/deferred_pass2.frag', decommenter))
        self.pass3_program = ShaderProgram(flat_vs, Shader.fromFile('fudge/renderers/deferred_pass3.frag', decommenter))
        self.pass4_program = ShaderProgram(flat_vs, Shader.fromFile('fudge/renderers/deferred_pass4.frag', decommenter))
        
        
        # set uniforms
        self.pass1_program.use(
            albedoTex = 0,
            normalTex = 1,
            factorTex = 2,
            rnm = 3,
            HEIGHT = float(self.height),
            WIDTH = float(self.width),
            # extra switches
            sw_AO = self.settings.AO,
            sw_ALL_LIGHTS = self.settings.ALL_LIGHTS,
            sw_EDGE = self.settings.EDGE,
            LIGHT_STRENGTH = self.settings.light_strength,
            LIGHT_DIST = self.settings.light_dist,
        )
    
    def Initialize(self):
        # OpenGL init
        DefaultRenderer.Initialize(self)
        glDisable(GL_LIGHTING)
        
        # deferred shading items
        self.__buildFBOs()
        self.__compileShaders()
        
        
        # rnm image
        self.rnm = Texture.FromImage("noise.bmp")
        
        # default textures
        self.default_textures = self.LoadAllTextures('fudge/renderers/default_.tga')
        
        # change FBO sizes on window resize
        renderer = self
        w = fudge.Window()
        @w.add_event_handler(WindowResize)
        def windowresize(self, e):
            renderer.width = e.width
            renderer.height = e.height
            renderer.__buildFBOs()

    def __RenderScene(self, e):
        e.scene._draw()
        
    def Render(self, e):
        # bind default textures
        self.default_textures.bindAll(0)
        # activate FBO and G-buffer program
        self.gbuffer_program.use(
            diffuse_texture = 0,
            specular_texture = 1,
            normal_texture = 2,
            sw_NMAP = self.settings.NMAP,
            sw_RELIEF = self.settings.RELIEF,
        )
        
        self.gbuffer_fbo.engage()
        
        
        # normal render
        glClearColor(0.0, 0.0, 0.0, 0.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        self.__RenderScene(e)

        # disable FBO and clear up
        self.gbuffer_fbo.disengage()

        # bind FBO textures + rnm texture 
        self.default_textures.unbindAll()
        self.gbuffer_fbo.bindTargets(0,1,2,3)
        self.rnm.bind(4)
        
        # activate deferred shader and set texture locations + switches
        self.pass1_program.use(
            albedoTex = 0,
            normalTex = 1,
            factorTex = 2,
            mvPosTex = 3,
            rnm = 4,
            # lights
            lights = self._lights,
            lights_color = self._lights_color,
            lights_dist = self._lights_dist,
            lights_spec = self._lights_spec,
            # feature switches/settings
            sw_AO = self.settings.AO,
            sw_ALL_LIGHTS = self.settings.ALL_LIGHTS,
            sw_EDGE = self.settings.EDGE,
            LIGHT_STRENGTH = self.settings.light_strength,
            LIGHT_DIST = self.settings.light_dist,
        )
        self.pass1_fbo.engage()
        
        # render gbuffer -> 1st pass
        self.gbuffer_fbo.render()
        
        # disable FBO and tear down gbuffer
        self.pass1_fbo.disengage()
        self.rnm.unbind()
        self.gbuffer_fbo.unbindTargets()
        
        # render 1st pass -> 2nd pass
        self.pass2_fbo.engage()
        self.pass1_fbo.bindTargets(0,1)
        self.pass2_program.use(
            shading_ssao = 0,
            overbright = 1,
        )
        
        self.pass1_fbo.render()

        self.pass2_fbo.disengage()
        self.pass1_fbo.unbindTargets()
        
        # render pass 2 -> pass 3
        self.pass3_fbo.engage()
        self.pass2_fbo.bindTargets(0, 1)
        self.pass3_program.use(
            shading_ssao_h = 0,
            overbright_h = 1,
        )
        self.pass2_fbo.render()
        
        self.pass3_fbo.disengage()
        self.pass2_fbo.unbindTargets()        
        
        # render pass 3 -> pass 4
        self.pass1_fbo.bindTargets(0, 1)
        self.pass3_fbo.bindTargets(2, 3)
        self.gbuffer_fbo.textures[1].bind(4)
        
        self.pass4_program.use(
            shading_ssao = 0,
            overbright = 1,
            shading_ssao_blur = 2,
            overbright_blur = 3,
            normalDepthTex = 4,
            sw_DOF = self.settings.DOF,
            sw_AO_ONLY = self.settings.AO_ONLY,
            sw_BLOOM = self.settings.bloom,
        )
        
        self.pass3_fbo.render()
        
        self.pass1_fbo.unbindTargets()
        self.pass3_fbo.unbindTargets()
        self.gbuffer_fbo.textures[1].unbind()
        
        # display
        glutSwapBuffers()
       
    @staticmethod
    def LoadAllTextures(template, textures = 'dsn'):
        '''
            Helper method for loading all the required textures. 
        '''
        t = []
        t.append(Texture.FromImage(template.replace('_.', '_d.') , mipmap = True) if 'd' in textures else NoTexture())
        t.append(Texture.FromImage(template.replace('_.', '_s.')) if 's' in textures else NoTexture())
        t.append(Texture.FromImage(template.replace('_.', '_n.')) if 'n' in textures else NoTexture())
        
        return TextureGroup(*t)

class Light(Empty):
    '''
        Deferred Light class, use it as a model. 
        Basic implementation, requires specifying which one of the 10 lights
    '''
    def __init__(self, slot, position = Vector3(), color = Vector3(1.0, 1.0, 1.0), distance = 1.0, specularity = 1.0, **kwargs):
        self._Position = Vector4(position.x, position.y, position.z, 1.0)
        self.slot = slot
        fudge.Renderer().get()._lights_dist[slot] = distance
        fudge.Renderer().get()._lights_color[slot] = color
        fudge.Renderer().get()._lights_spec[slot] = specularity
        self._children = []
    
    def _transform(self):
        glTranslatef(*self._Position[:3])
    
    def _internal_draw(self):
        mv_matrix = Matrix4(glGetDoublev(GL_MODELVIEW_MATRIX))
        fudge.Renderer().get()._lights[self.slot] = self._Position * mv_matrix
    